
#ifndef P2P_NODETABLE_HPP
#define P2P_NODETABLE_HPP
#include "Common.hpp"
#include "../muduo/net/InetAddress.h"
#include "../PrivateKey.hpp"
#include "../PublicKey.hpp"
#include "params.hpp"
#include "../spdlog/spdlog.h"
#include <list>
#include <array>
#include <unordered_map>
#include <unordered_set>
#include <algorithm>
#include <mutex>
#include <cmath>


static const int bucketTypeNew = 0x01;
static const int bucketTypeOld = 0x02;

class KnowAddress
{
public:
    muduo::net::InetAddress addr;
    muduo::net::InetAddress src;
    int attempts;
    muduo::Timestamp lastAttempt;
    muduo::Timestamp lastSuccess;
    int bucketType;
    std::vector<int> buckets;

    KnowAddress(muduo::net::InetAddress const& _addr,muduo::net::InetAddress const& _src);
    KnowAddress(const KnowAddress& addr) = default;
    bool isOld() { return bucketType == bucketTypeOld; }
    bool isNew() { return bucketType == bucketTypeNew; }
    void markAttemp(){ lastAttempt = muduo::Timestamp::now(); attempts++; }
    void markGood(){ lastAttempt = muduo::Timestamp::now(); lastSuccess=lastAttempt; attempts=0; }

    int addBucketRef(int bucketIdx);
    int removeBucketRef(int bucketIdx);

    KnowAddress& pickOldest(int bucketType, int bucketIdx);
    /*
    An address is bad if the address in question is a New address, has not been tried in the last
    minute, and meets one of the following criteria:

    1) It claims to be from the future
    2) It hasn't been seen in over a week
    3) It has failed at least three times and never succeeded
    4) It has failed ten times in the last week

    All addresses that meet these criteria are assumed to be worthless and not
    worth keeping hold of.

    XXX: so a good peer needs us to call MarkGood before the conditions above are reached!
    */
    bool isBad();
};
class NodeTable 
{
public:
    NodeTable(std::string filePath, bool routabilityStrict, 
                    std::shared_ptr<spdlog::logger> logger = spdlog::stdout_color_mt("NodeTable"));
    // Add our own addresses so we will not add ourselves
    void addOurAddress(muduo::net::InetAddress const&  addr);
    // Check if it is our address
    bool isOurAddress(muduo::net::InetAddress const&  addr);

    void addAddress(muduo::net::InetAddress const& addr, muduo::net::InetAddress const& src);
    void removeAddress(muduo::net::InetAddress const &addr);

    bool hasAddress(muduo::net::InetAddress const& addr);

    bool needMoreAddress();

    int calcNewBucket(const muduo::net::InetAddress &addr, const muduo::net::InetAddress& src);
    int calcOldBucket(const muduo::net::InetAddress &addr);

    muduo::net::InetAddress const& pickAddress(int biasTowardsNewAddrs);

    // Mark address TODO:
    void markGood(muduo::net::InetAddress const& addr);
    void markAttemp(muduo::net::InetAddress const& addr);
    void markBad(muduo::net::InetAddress const& addr); 
    bool isGood(muduo::net::InetAddress const& addr);

    std::vector<muduo::net::InetAddress> getSelection();
    std::vector<muduo::net::InetAddress> getSelectionWithBias(int biasTowardsNewAddrs); //TODO:
    // Save the address to disk
    void save(); //TODO:

    int sizeWithMutex(){ std::unique_lock<std::mutex> lock(mtx_); return sizeWithoutMutex(); }

    // A copy of the new and old bucket address
    std::vector<std::shared_ptr<KnowAddress>> listOfKnownAddress();

private:
    int sizeWithoutMutex() { return nNew+nOld; }

    std::unordered_map<std::string, std::shared_ptr<KnowAddress> >& getBucket(int bucketType, int bucketIdx);
    
    // Adds ka to new bucket. Returns false if it couldn't do it cuz buckets full.
    // NOTE: currently it always returns true.
    bool addToNewBucket(std::shared_ptr<KnowAddress>& ka, int bucketIdx);
    // Adds ka to old bucket. Returns false if it couldn't do it cuz buckets full.
    bool addToOldBucket(std::shared_ptr<KnowAddress>& ka, int bucketIdx);

    void removeFromBucket(std::shared_ptr<KnowAddress>& ka,int bucketType, int bucketIdx);
    void removeFromAllBucket(std::shared_ptr<KnowAddress>& ka);

    void expireNew(int bucketIdx);

    std::shared_ptr<KnowAddress> pickOldest(int bucketType, int bucketIdx);

    void moveToOld(std::shared_ptr<KnowAddress> &ka);

    std::string filePath_;
    bool rotabilityStrict_;
    std::shared_ptr<spdlog::logger> logger_;

    std::string key_;

    std::mutex mtx_;
    std::unordered_set<std::string> ourAddr_;
    std::map<std::string, std::shared_ptr<KnowAddress> > addrLookup_; // new & old address;
    std::array<std::unordered_map<std::string, std::shared_ptr<KnowAddress>>, kNewBucketCount> bucketsNew_;
    std::array<std::unordered_map<std::string, std::shared_ptr<KnowAddress>>, kOldBucketCount> bucketsOld_;
    int nOld;
    int nNew;
};


#endif // !P2P_NODETABLE_HPP
